package com.zty.tool.excel;

import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.WriteTable;
import com.alibaba.excel.write.metadata.WriteWorkbook;
import com.zty.tool.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.*;

/**
 * easyExcel数据导出模板（可以支持百万数据导出）
 * 采用一种模板的模式，内含Excel导出通用的逻辑
 * 这个类定义了一个导出Excel文件的流程，包括准备数据、写入数据到不同的Sheet页，并处理大数据量的情况。
 * 它通过分批写入数据来避免内存溢出，并且可以处理超大文件的导出。
 * 这个类是抽象的，因为它定义了一些必须由子类实现的抽象方法，
 * 这些方法是与业务逻辑相关的，例如获取数据总数、构建数据列表等。
 *
 * @author: zty
 * @date: 2023/3/5
 */
@Slf4j
// 定义一个泛型抽象类，这个类提供了导出数据到Excel的基本框架。
public abstract class BaseEasyExcelExport<T> {

    /**
     * 启动导出Excel的过程
     * @param fileName 文件名
     * @param queryCondition 查询条件
     */
    protected void exportExcel(String fileName, Map<String, Object> queryCondition) {

        // 从Spring上下文中获取HttpServletResponse对象，这是为了将生成的Excel文件发送给客户端。
        HttpServletResponse response = SpringContextUtils.getHttpServletResponse();

        // 根据查询条件获取数据的总记录数。
        Long totalCount = dataTotalCount(queryCondition);

        // 获取每个Excel Sheet页应该包含的记录数。
        Long sheetDataRows = eachSheetTotalCount();

        // 获取每次写入Sheet页的数据量。
        Long writeDataRows = eachTimesWriteSheetTotalCount();

        // 如果总记录数小于每个Sheet页的记录数，则调整Sheet页的记录数为总记录数。
        if (totalCount < sheetDataRows) {
            sheetDataRows = totalCount;
        }

        // 如果每个Sheet页的记录数小于每次写入的数据量，则调整每次写入的数据量为Sheet页的记录数。
        if (sheetDataRows < writeDataRows) {
            writeDataRows = sheetDataRows;
        }

        // 调用doExport方法开始导出过程。
        doExport(response, fileName, queryCondition, totalCount, sheetDataRows, writeDataRows);
    }


    /**
     * 实际的导出逻辑
     *
     * @param response
     * @param fileName       要导出的文件名
     * @param queryCondition 查询的条件
     * @param totalCount     查询出的数量
     * @param sheetDataRows  整体的页数
     * @param writeDataRows  写出的条数
     */
    private void doExport(HttpServletResponse response,
                          String fileName,
                          Map<String, Object> queryCondition,
                          Long totalCount,
                          Long sheetDataRows,
                          Long writeDataRows) {
        OutputStream outputStream = null;
        try {
            // 获取响应的输出流，准备写入数据。
            outputStream = response.getOutputStream();

            // 配置Excel写入器。
            WriteWorkbook writeWorkbook = new WriteWorkbook();
            writeWorkbook.setOutputStream(outputStream);
            writeWorkbook.setExcelType(ExcelTypeEnum.XLSX);
            ExcelWriter writer = new ExcelWriter(writeWorkbook);

            // 配置Excel表格的头部。
            WriteTable table = new WriteTable();
            table.setHead(getExcelHead());

            // 计算需要的Sheet数量。
            Long sheetNum = totalCount % sheetDataRows == 0 ? (totalCount / sheetDataRows) : (totalCount / sheetDataRows + 1);

            // 计算通常情况下每个Sheet需要写入的次数。
            Long oneSheetWriteCount = sheetDataRows / writeDataRows;
            if (totalCount <= sheetDataRows || totalCount % writeDataRows > 0) {
                oneSheetWriteCount++;
            }

            // 计算最后一个Sheet需要写入的次数。
            Long lastSheetWriteCount = totalCount % sheetDataRows == 0 ? oneSheetWriteCount :
                    (totalCount % sheetDataRows % writeDataRows == 0 ? (totalCount % sheetDataRows / writeDataRows) :
                            (totalCount % sheetDataRows / writeDataRows + 1));

            // 分批查询和分次写入数据。
            List<List<String>> dataList = new ArrayList<>();
            for (int i = 0; i < sheetNum; i++) {
                WriteSheet sheet = new WriteSheet();
                sheet.setSheetNo(i);
                sheet.setSheetName(sheetNum == 1 ? fileName : fileName + i);

                // 计算当前Sheet页需要写入的次数。
                Long currentSheetWriteCount = (i != sheetNum - 1 || i == 0) ? oneSheetWriteCount : lastSheetWriteCount;

                for (int j = 0; j < currentSheetWriteCount; j++) {
                    dataList.clear();

                    // 构建当前批次的数据列表。
                    buildDataList(dataList, queryCondition, j + 1 + oneSheetWriteCount * i, writeDataRows);

                    // 将数据写入到当前Sheet页。
                    writer.write(dataList, sheet, table);
                }
            }

            // 设置响应头，告诉浏览器这是一个文件下载的响应。
            response.setHeader("Content-Disposition", "attachment;filename="
                    + URLEncoder.encode(fileName, "UTF-8") + ".xlsx");
            response.setContentType("application/vnd.ms-excel");
            response.setCharacterEncoding("utf-8");

            // 完成写入并刷新输出流。
            writer.finish();
            outputStream.flush();
        } catch (Exception e) {
            // 如果出现异常，记录错误日志。
            log.error("AbstractEasyExcelExport.exportWithBigData.error:{}", e.getMessage(), e);
        } finally {
            // 最后，无论是否出现异常，都关闭输出流。
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (Exception e) {
                    log.error("AbstractEasyExcelExport.exportWithBigData.close.error:{}", e.getMessage(), e);
                }
            }
        }
    }

    // 以下是一些抽象方法，需要子类具体实现。

    // 获取Excel表头的抽象方法。
    protected abstract List<List<String>> getExcelHead();

    // 计算数据总数的抽象方法。
    protected abstract Long dataTotalCount(Map<String, Object> conditions);

    // 获取每个Sheet页的记录数的抽象方法。
    protected abstract Long eachSheetTotalCount();

    // 获取每次写入Sheet页的记录数的抽象方法。
    protected abstract Long eachTimesWriteSheetTotalCount();

    // 构建每次查询的数据列表的抽象方法。
    protected abstract void buildDataList(List<List<String>> resultList, Map<String, Object> queryCondition,
                                          Long pageNo, Long pageSize);

}